En PyTorch, un tensor es una estructura de datos central que se utiliza para almacenar y operar sobre datos numéricos. Los tensores son una generalización de matrices y vectores a más dimensiones y son similares en concepto a los arrays en NumPy, pero con capacidades adicionales que los hacen adecuados para el aprendizaje profundo. Aquí están algunas características clave de los tensores en PyTorch:
Multidimensionalidad: Un tensor en PyTorch puede tener varias dimensiones. Por ejemplo, un tensor 0D es un escalar, un tensor 1D es un vector, un tensor 2D es una matriz, y un tensor con tres o más dimensiones puede representar datos más complejos como imágenes (3D: alto, ancho, canal) o secuencias de datos temporales.
Tipo de Datos Homogéneos: Todos los elementos en un tensor PyTorch son del mismo tipo de dato (por ejemplo, float32
, int64
, etc.). PyTorch soporta varios tipos de datos que permiten controlar el tamaño y el comportamiento en cuanto a la precisión y el rango.
Compatibilidad con GPU: A diferencia de los arrays de NumPy que generalmente residen en la memoria de la CPU, los tensores de PyTorch pueden ser trasladados a la memoria de una GPU para permitir cálculos de alto rendimiento, lo cual es fundamental en el entrenamiento de modelos de aprendizaje profundo.
Diferenciación Automática: Los tensores en PyTorch pueden llevar un registro de las operaciones realizadas sobre ellos para permitir la diferenciación automática. Esta característica es clave para la implementación de algoritmos de aprendizaje automático, especialmente en la retropropagación en redes neuronales.
Interoperabilidad con NumPy: PyTorch ofrece una buena interoperabilidad con los arrays de NumPy, permitiendo convertir fácilmente entre arrays de NumPy y tensores de PyTorch.
Funcionalidades para Aprendizaje Profundo: PyTorch proporciona una amplia gama de operaciones y métodos predefinidos para manipular tensores, incluyendo operaciones matemáticas, de álgebra lineal, transformaciones, y más, lo que lo hace muy adecuado para tareas de aprendizaje automático y aprendizaje profundo.
Veamos cómo podemos crear y manipular tensores en PyTorch.
import torch
x = torch.tensor([1, 2, 3, 4])
print(x)
tensor([1, 2, 3, 4])
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(x)
tensor([[1, 2, 3], [4, 5, 6]])
x = torch.zeros(2, 3)
print(x)
tensor([[0., 0., 0.], [0., 0., 0.]])
x = torch.ones(2, 3, dtype=torch.int)
print(x)
tensor([[1, 1, 1], [1, 1, 1]], dtype=torch.int32)
x = torch.empty(2, 3)
print(x)
tensor([[0., 0., 0.], [0., 0., 0.]])
Hemos declarado una matriz vacía (no inicializada), por tanto, no contiene valores conocidos antes de su uso. Cuando se crea una matriz no inicializada, los valores que estaban en la memoria asignada en ese momento aparecerán como valores iniciales.
x = torch.rand(5, 3)
print(x)
tensor([[0.2860, 0.8601, 0.0864], [0.3273, 0.2752, 0.7389], [0.7438, 0.9292, 0.6280], [0.7182, 0.7319, 0.8711], [0.8218, 0.8293, 0.7393]])
A diferencia de torch.empty
, torch.rand
genera unos valores aleatorios entre 0 y 1.
Podemos crear un tensor basado en un tensor existente. Estos métodos reutilizarán propiedades del tensor de entrada, p. ej. dtype o device, a menos que el usuario proporcione nuevos valores.
y1 = torch.ones_like(x)
y2 = torch.zeros_like(x, dtype=torch.int)
y3 = torch.rand_like(x)
print(x)
print(y1)
print(y2)
print(y3)
tensor([[0., 0., 0.], [0., 0., 0.]]) tensor([[1., 1., 1.], [1., 1., 1.]]) tensor([[0, 0, 0], [0, 0, 0]], dtype=torch.int32) tensor([[0.0457, 0.3236, 0.1809], [0.5337, 0.2979, 0.9751]])
Hay varias sintaxis para las operaciones. En el siguiente ejemplo, veremos la operación de suma.
x = torch.ones(2, 3) * 2
y = torch.ones(2, 3) * 3
print(x + y)
tensor([[5., 5., 5.], [5., 5., 5.]])
print(torch.add(x, y))
tensor([[5., 5., 5.], [5., 5., 5.]])
result = torch.empty(2, 3)
torch.add(x, y, out=result)
print(result)
tensor([[5., 5., 5.], [5., 5., 5.]])
Operaciones in_place. Cuando encontramos el sufijo "_" en cualquier método correspondiente a una operación de un objeto, significa que el resultado de la operación es almacenado en el propio objeto, reemplazando al valor anterior.
y.add_(x)
print(y)
tensor([[5., 5., 5.], [5., 5., 5.]])
¡OJO! y = y + x
no es una operación in_place.
x = torch.ones(2, 3) * 2
y = torch.ones(2, 3) * 3
print(id(x))
print(id(y))
y = y + x
print(id(y))
4728620192 6051841424 6051841344
x = torch.ones(2, 3) * 2
y = torch.ones(2, 3) * 3
print(id(x))
print(id(y))
y.add_(x)
print(id(y))
6051980416 6051976576 6051976576
Sin embargo, y += x
sí es una operación in_place igual a y.add_(x)
. Por tanto, +=
es el operador sobrecargado de add_()
.
x = torch.ones(2, 3) * 2
y = torch.ones(2, 3) * 3
print(id(x))
print(id(y))
y += x
print(id(y))
6051652416 5631564576 5631564576
En Python no es así.
x=1
y=2
print(id(x))
print(id(y))
y += x
print(id(y))
4372529392 4372529424 4372529456
Al clonar un tensor se generará un nuevo tensor con los mismos datos que el tensor original, pero en una ubicación de memoria diferente. Esto significa que modificar el tensor clonado no afectará al tensor original, y viceversa.
x = torch.ones(2, 3) * 2
y = x.clone()
print(x is y)
print(id(x))
print(id(y))
False 6051978896 6048149520
Ten en cuenta que el siguiente código no funcionaría como se cabría esperar. El cambio de y
también cambiará x
. Ambos tensores comparten el mismo espacio de memoria. Por tanto, y
es un alias de x
.
x = torch.ones(2, 3) * 2
y = x
print(x is y)
print(id(x))
print(id(y))
True 6051975456 6051975456
El "slicing" (segmentación) permite extraer, seleccionar o manipular ciertas partes de los tensores de forma eficiente y conveniente mediante una sintaxis determinada.
import torch
tensor = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("Tensor Original:\n", tensor)
Tensor Original: tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Seleccionar la primera fila
primera_fila = tensor[0]
print("Primera Fila:", primera_fila)
# Seleccionar la segunda columna
segunda_columna = tensor[:, 1]
print("Segunda Columna:", segunda_columna)
Primera Fila: tensor([1, 2, 3]) Segunda Columna: tensor([2, 5, 8])
# Seleccionar las primeras dos filas
primeras_dos_filas = tensor[:2]
print("Primeras dos filas:\n", primeras_dos_filas)
# Seleccionar las últimas dos columnas
ultimas_dos_columnas = tensor[:, -2:]
print("Últimas dos columnas:\n", ultimas_dos_columnas)
Primeras dos filas: tensor([[1, 2, 3], [4, 5, 6]]) Últimas dos columnas: tensor([[2, 3], [5, 6], [8, 9]])
También podemos hacer una selección basada en una condición booleana.
# Seleccionar elementos mayores que 5
mayores_que_cinco = tensor[tensor > 5]
print("Elementos mayores que 5:", mayores_que_cinco)
Elementos mayores que 5: tensor([6, 7, 8, 9])
# Seleccionar elementos pares y cambiar su valor a 0
tensor[tensor % 2 == 0] = 0
print("Tensor modificado:\n", tensor)
Tensor modificado: tensor([[1, 0, 3], [0, 5, 0], [7, 0, 9]])
Es útil contar con funciones que nos permitan seleccionar la triangular superior o inferior de una matriz cuadrada.
print("Tensor triangular superior:\n", torch.triu(tensor))
Tensor triangular superior: tensor([[1, 2, 3], [0, 5, 6], [0, 0, 9]])
El tensor vista comparte los mismos datos subyacentes que su tensor base. Soportar vistas evita la copia explícita de datos, lo que nos permite realizar cambios de forma, segmentación y operaciones elemento a elemento de manera rápida y eficiente en memoria.
x = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9,10,11,12]]).contiguous()
y = x.view(6, 2)
x[0,0] = 555
print(x)
print(y)
print(x is y)
tensor([[555, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12]]) tensor([[555, 2], [ 3, 4], [ 5, 6], [ 7, 8], [ 9, 10], [ 11, 12]]) False
print(x is y)
False
Devuelve un tensor con los mismos datos y la misma cantidad de elementos que la entrada, pero con la forma especificada. Cuando sea posible, el tensor devuelto será una vista de la entrada. De lo contrario, será una copia.
x = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9,10,11,12]])
y = x.reshape(6, 2)
x[0,0] = 555
print(x)
print(y)
print(x is y)
tensor([[555, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12]]) tensor([[555, 2], [ 3, 4], [ 5, 6], [ 7, 8], [ 9, 10], [ 11, 12]]) False
El tensor de PyTorch y la matriz de NumPy comparten las mismas ubicaciones de memoria subyacentes cuando el tensor está en CPU (recordemos que PyTorch puede ejecutarse tanto en CPU o GPU). Así que, si modificamos los valores del array modificaremos los valores del tensor.
a = torch.ones(5)
print(a)
b = a.numpy() # Creamos un array de numpy desde un tensor de pytorch
print(b)
a.add_(1)
print("Tensor pytorch: ", a)
print("Array numpy: ", b)
tensor([1., 1., 1., 1., 1.]) [1. 1. 1. 1. 1.] Tensor pytorch: tensor([2., 2., 2., 2., 2.]) Array numpy: [2. 2. 2. 2. 2.]
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a) # Creamos un tensor de pytorch desde un array de numpy
np.add(a, 1, out=a)
print(a)
print(b)
[2. 2. 2. 2. 2.] tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
Utilizamos el método .item()
para obtener el valor de un tensor de un solo elemento o escalar como un número de Python.
a = torch.tensor([555])
print(a)
b = a.item()
print(b)
tensor([555]) 555
Podemos mover tensores a cualquier dispositivo (GPU) mediante el método .to
# let us run this cell only if CUDA is available
# We will use ``torch.device`` objects to move tensors in and out of GPU
if torch.cuda.is_available():
device = torch.device("cuda") # a CUDA device object
y = torch.ones_like(x, device=device) # directly create a tensor on GPU
x = x.to(device) # or just use strings ``.to("cuda")``
z = x + y
print(z)
print(z.to("cpu", torch.double)) # ``.to`` can also change dtype together!
tensor([0.2168], device='cuda:0') tensor([0.2168], dtype=torch.float64)